﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.ApplicationServer.Caching;
using System.Diagnostics.Contracts;
using YasharEl.Infrastructure.Caching.Orm;

namespace YasharEl.Infrastructure.Caching.AppFabric.Orm
{
    public class AppFabricOrmCacheManager : AppFabricCacheManager, IOrmCacheManager
    {
        #region Fields

        private readonly string _locksRegionName;
        private readonly int _lockTimeout;

        private readonly IDictionary<string, DataCacheLockHandle> _lockHandles;
        private readonly DataCache _locksCache;

        #endregion

        #region Properties

        public string LocksRegionName
        {
            get { return _locksRegionName; }
        }

        #endregion

        #region Constructors

        public AppFabricOrmCacheManager(string name, ISerializationProvider serializationProvider, string regionName,
            string locksRegionName, int lockTimeout, DataCache dataCache, DataCache locksDataCache)
            : base(name, serializationProvider, regionName, dataCache)
        {
            Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(locksRegionName), "locksRegionName could not be null or empty.");
            Contract.Requires<ArgumentNullException>(locksDataCache != null, "locksDataCache could not be null.");

            _locksRegionName = locksRegionName;
            _lockTimeout = lockTimeout;
            _lockHandles = new Dictionary<string, DataCacheLockHandle>();
            _locksCache = locksDataCache;
        }

        #endregion

        #region IOrmCacheManager Implementations

        public void Lock(string key)
        {
            Lock(key, true);
        }

        public void Unlock(string key)
        {
            try
            {
                if (_lockHandles.ContainsKey(key.ToString()))
                {
                    _locksCache.Unlock(key.ToString(), _lockHandles[key.ToString()]);

                    lock (_lockHandles)
                    {
                        _lockHandles.Remove(key.ToString());
                    }
                }
            }
            catch (DataCacheException ex)
            {
                if (ex.ErrorCode != DataCacheErrorCode.RegionDoesNotExist)
                    throw new CacheException("Error occurred when trying to unlock object from the cache.", ex);
            }
        }

        public void Destroy()
        {
            try
            {
                DataCache.RemoveRegion(RegionName);
            }
            catch (DataCacheException ex)
            {
                if (ex.ErrorCode != DataCacheErrorCode.RegionDoesNotExist)
                    throw new CacheException("Error occurred when trying to destroy the cache.", ex);
            }
        }

        /// <summary>
        /// Specifies the timeout in milliseconds when locking items in the cache.
        /// </summary>
        public int LockTimeout
        {
            get { return _lockTimeout; }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Attempts to lock the key in the locks region and stores a reference to the lock handle in memory.
        /// If the lock region doesn't exists, it will attempt to create it.
        /// </summary>
        /// <param name="key">The key to lock.</param>
        /// <param name="createMissingRegion">A flag to determine whether or not to create the lock region if it doesn't
        /// exist.</param>
        private void Lock(object key, bool createMissingRegion)
        {
            DataCacheLockHandle lockHandle = null;

            try
            {
                _locksCache.GetAndLock(
                    key.ToString(),
                    TimeSpan.FromMilliseconds(LockTimeout),
                    out lockHandle,
                    true
                );

                lock (_lockHandles)
                {
                    _lockHandles.Add(key.ToString(), lockHandle);
                }
            }
            catch (DataCacheException ex)
            {
                if (ex.ErrorCode == DataCacheErrorCode.RegionDoesNotExist && createMissingRegion)
                {
                    CreateLocksRegion(b => Lock(key, false));
                }
                else
                {
                    throw new CacheException("Error occurred when trying to lock object in the cache.", ex);
                }
            }
        }

        /// <summary>
        /// Creates the locks AppFabric cache region.
        /// </summary>
        /// <param name="callback">A call back to call once the region has been created.</param>
        private void CreateLocksRegion(Action<bool> callback)
        {
            CreateRegion(_locksCache, LocksRegionName, callback);
        }

        /// <summary>
        /// Creates an AppFabric cache region.
        /// </summary>
        /// <param name="cache">The data cache [client] to use to create the cache region.</param>
        /// <param name="regionName">The name of the AppFabric cache region to create.</param>
        /// <param name="callback">A call back to call once the region has been created.</param>
        private void CreateRegion(DataCache cache, string regionName, Action<bool> callback)
        {
            try
            {
                callback(cache.CreateRegion(regionName));
            }
            catch (DataCacheException ex)
            {
                if (ex.ErrorCode == DataCacheErrorCode.RegionAlreadyExists)
                    callback(false);
                else
                {
                    throw new CacheException(string.Format("Error occurred when trying create region with name {0}.", regionName), ex);
                }
            }
        }

        #endregion
    }
}
